Skip to main content

Advanced animation

Basics

Way 1

Consider to use to get the highest frame-rate and smooth animations

with TransitionType set to None if the calculation time does not exceed the 1/60 of the second. Otherwise consider to use "Linear" interpolation option and a small amount of TransitionDuration around 10-100 depending on how long it takes to update the data.

tip

If you animation looks sloppy, you can always cheat asking Javascript to interpolate between data portions over the time. Use TransitionType and TransitionDuration

For example

cell 1
balls = RandomReal[{-1,1}, {100,3}];
vels = RandomReal[{-1,1}, {100,3}];

Graphics3D[{
Table[With[{i = i},
{
RGBColor[RandomReal[{0,1}, 3]],
Sphere[balls[[i]] // Offload, 0.03]
}
], {i, Length[balls]}],
AnimationFrameListener[balls // Offload, "Event"->"frame"]
}]

evaluate the cell above. It will create a canvas with randomly scattered balls

At the start of the browser's frame, an event "frame" is triggered to request an update of data from the Kernel. However, after this, you'll need to "recharge" an AnimationFrameListener, otherwise it will not trigger the event again. This can be automated to occur whenever a change in the balls symbol is detected.

This process ensures the following benefits:

  • Synchronization of animation with the browser's engine (eliminating flickering).
  • Ability to skip frames if recalculations take longer than one frame of your browser, adapting to your computing power.

Here is our update function

EventHandler["frame", Function[Null,

vels = Table[
If[Norm[balls[[i]]] < 0.01, -1, 1] vels[[i]] - 0.08 balls[[i]]
, {i, Length[balls]}];

balls = Table[balls[[i]] + 0.08 vels[[i]], {i, Length[balls]}];
]];

To start an animation - reevaluate cell 1 or use this "kickstarter"

EventFire["frame", Null]

Way 2

Consider to use SetInterval for simple or resource intensive animation. Set TransitionDuration and TransitionType to a proper value to interpolate the values.

Usually if your SetInterval is let's say 100 ms, then TransitionDuration should be around 100 ms as well to get the smoothest animation.

tip

If you animation looks sloppy, you can always cheat asking Javascript to interpolate between data portions over the time. Use TransitionType and TransitionDuration

For example

ParametricAnimator[equation_, variable_:t, range_:{0, Infinity, 0.1}] := LeakyModule[{time = range[[1]], task, scale = 1, array = {}, scaledArray={}, cell = ResultCell[]},

(* sample the equation each frame and rescale if needed *)
animate := Block[{variable = time},
With[{e = {Sin[t], Cos[t]} equation},
scale = If[Norm[e scale] > 1.4, scale 0.95, scale 1];
array = Append[array, e];
scaledArray = scale array;
pointer = e scale;
];

time += range[[3]];
If[time >= range[[2]], TaskRemove[task]];
];

animate;

(* async task to animate every 50 ms *)
task = SetInterval[animate, 50];

(* stop the task if cell was destroyed or reevaluated *)
EventHandler[cell, {"Destroy"->Function[Null, TaskRemove[task]; Print["removed"]]}];

Graphics[{Red, PointSize[0.05], Point[pointer // Offload],
Opacity[0.5], Line[scaledArray // Offload]
}, TransitionDuration->50, TransitionType->"Linear", Controls->False, PlotRange->{{-1,1}, {-1,1}}]
]

This will sample a given parametric equation and animate it with 50 ms time step, while on Javascript's side it will interpolate between frames, so that overall animation will look smooth and will be rendered at 60FPS

ParametricAnimator[Exp[Sin[t]] - 2 Cos[4t] + Sin[(2t - Pi)/24], t, {0,16, 0.05}]

Way 3

If you animation depends on some interaction with a user, it might be a good idea to run it and update objects attributes only, when some event is fired.

For example

pt = {0,0};
Graphics[{
White,
EventHandler[
Rectangle[{-2,-2}, {2,2}],
{"mousemove"->Function[xy, pt = xy]}
],
PointSize[0.05], Cyan,
Point[pt // Offload]
}]

a mouse follower

A remark on color and opacity

RGBColor as well as Opacity do support dynamic updates in the context of Graphics. Here it is a bit tricky, since all graphics symbols sharing the same scope should bind to them indirectly. The good news, you do not have to think about and just

color = {1,0,0};
Graphics[{RGBColor[color // Offload], Disk[{0,0}, 1]}]

EventHandler[InputJoystick[], Function[xy,
color = Normalize[{xy[[1]], xy[[2]], 0.5}] // Abs;
]]

Or even more complicated - combining it together with traditional dynamics with nested variables

opacity = 0.5;
Graphics[{Opacity[Offload[opacity]], Red, Disk[{0,0}, Offload[1-opacity]], Blue, Opacity[Offload[1.0 - opacity]], Disk[{0,0}, Offload[opacity]]}, ImagePadding->None]

EventHandler[InputRange[0,1,0.1], Function[value,
opacity = value;
]]

Creating and removing objects

The most examples given on the pages Dynamics, AnimationFrameListener considers only changing the attributes of created graphics primitives on the screen. One can also use pure raster graphics together with Image, however, this is quite cumbersome to deal with.

However, you have seen MetaMarker, you might know the solution. The technique of selecting already evaluated entities on the frontend and evaluating or removing other symbols inside its context comes handy here.

Simple example

Here we will append colorful Disk s to a Graphics symbol context following the mouse position. As usual the best way to do it is to use white Rectangle 😀

cell 1
Graphics[{White, EventHandler[Rectangle[{-1,-1}, {1,1}], {"mousemove"->handler}], MetaMarker["canvas"]}, ImagePadding->None]

The last thing is to define handler function

With[{win = CurrentWindow[]},
handler = Function[xy,
FrontSubmit[{
Hue[RandomReal[{0,1}], 1,1],
Disk[xy, RandomReal[{0.01,0.1}]]
}, MetaMarker["canvas"], "Window"->win]
];
];

Here we use sort of selector MetaMarker, which makes sure, that the result will be evaluated in the context of all instances marked as canvas. The next odd thing is CurrentWindow. We need this otherwise our anonymous function does not know to which window it should send an expression

Animating bubbles

We can go further and not only animate bubbles, but also remove them, when there are to many of them.

The complexity of many instances comes mostly from the fact, that we don't have a direct access from Wolfram Kernel to the frontend, but only WebSockets protocol, which gives a big overhead. The only solution to maintain good performance is to minimize numbers of transactions.

Think about it was a GPU-CPU communication, which also comes with similar restrictions.

Pool of objects

This is going to be our buffers

cPool = Table[{0.,0.}, {i,100}]; (* positions *)
vPool = cPool; (* velocities *)
rPool = Table[0., {i,100}]; (* radius or lifetime *)

oPool = Table[Null, {i,100}]; (* references to objects *)

The general idea is not to allocate new variables for new object, but rather reuse objects from the pool.

Graphical output is going to be the same

Graphics[{White, EventHandler[Rectangle[{-1,-1}, {1,1}], {"mousemove"->handler}], MetaMarker["canvas"]}, ImagePadding->None]

Our future animation loop is going to look like this

With[{win = CurrentWindow[]},
handler = Function[xy,
If[!created[xy, "canvas", win], update[win]];
];
];

We don't need to evaluate it now

An update functions - just go over our arrays and produce new

update[win_] := With[{},
{cPool, rPool} = Transpose[MapIndexed[Function[{a, index},
(* if slot is not empty - recalculate *)
If[oPool[[index//First]] =!= Null,

If[a[[2]] <= 0.002,
(* if radius is too small - remove an object *)
remove[index//First, win];
a
,
(* if ok - animate *)
{a[[1]] + 0.05 vPool[[index//First]], 0.9 a[[2]]}
]

,
a
]

], {cPool, rPool} // Transpose]];
];

if a lifetime is close to zero, we need to remove created instance and free some slots in our buffers for new objects

remove[index_, win_] := (
(* destroy instance on the frontend *)
Delete[oPool[[index]], "Window"->win];
oPool[[index]] = Null
);

And finally a function to create new objects

created[xy_, marker_String, win_] := With[{
(* find empty slot *)
slot = FirstPosition[oPool, Null]
},
If[!MissingQ[slot],
With[{s = slot // First},

(* initial positions and etc *)
cPool[[s]] = xy;
rPool[[s]] = 0.05;
vPool[[s]] = RandomReal[{-1,1}, 2];
oPool[[s]] = True;

(* update so that object wont appear in an odd way *)
update[win];

(* create an instance of Disk on a graph *)
With[{
o = {
Hue[RandomReal[{0,1}],1,1],
(* prevent double updates *)
Disk[Offload[cPool[[s]]], Offload[rPool[[s]], "Static"->True]]
}
},
oPool[[s]] = FrontSubmit[o, MetaMarker[marker], "Window"->win, "Tracking"->True];
];
];

True
,
False
]
]

The big difference to the previous example Simple example is that we track our created instances using an options "Tracking", so that we can remove them later for our SVG canvas (aka Graphics)

All positions and radiuses are passed in two solid symbols cPool and rPool, then we only need to perform two data transactions to our frontend, which saves a lot of resources, when it comes to make objects flying on the screen. Because of the payload matters less, than each act of transactions in terms of the transport load.